--- title: "06-Lua 深度学习笔记" created: 2025-12-12 aliases: - Lua 深度学习笔记 tags: - 项目 --- # Lua 深度学习笔记 ## 第一部分:Lua 是什么?(顶层认知) ### 1.1 一句话定义 **Lua 是一种轻量级、快速、高效的编程语言。** 但这太抽象了。让我们用更有趣的方式理解。 ### 1.2 类比理解 ```text 如果Java是"办公楼" ├─ 功能完整 ├─ 大而全 ├─ 启动慢 └─ 规则复杂 那么Lua就是"便利店" ├─ 功能基础 ├─ 小而精 ├─ 响应快 └─ 规则简单 ``` **关键点:** Lua 被设计成可以嵌入到其他程序中运行的脚本语言。 ### 1.3 Lua 在产业中的位置 ```text 编程语言谱系图 系统语言 │ ┌──────────┼──────────┐ │ │ │ C/C++ Java Go (低阶,快速) (中阶,通用) (并发) 脚本语言 │ ┌──────────┼──────────┐ │ │ │ Python JavaScript Lua (通用脚本) (Web脚本) (嵌入脚本) Lua的核心特点:轻量级 + 可嵌入 ``` --- ## 第二部分:为什么需要 Lua?(问题导向) ### 2.1 现实场景分析 #### 场景 1:在 Redis 中执行原子操作 **问题:** 我需要在 Redis 中做"读-判断-写"三个操作,必须**不可分割**(原子性)。 ```java // Java中分开执行(不安全) int count = redis.get("counter"); // 读 if (count > 10) { // 判断 redis.set("flag", "yes"); // 写 } // 问题:在读和写之间,其他线程可能修改了count // 导致多个线程同时进入if,数据混乱 ``` **Lua 的解决方案:** 把所有操作放在一个 Lua 脚本里,Redis 执行脚本时是原子的。 ```lua -- checkNeedCaptcha.lua local count = redis.call('get', 'counter') if count > 10 then redis.call('set', 'flag', 'yes') return 'true' else redis.call('set', 'flag', 'no') return 'false' end -- Redis 保证整个脚本从开始到结束不会被中断 ``` #### 场景 2:减少网络往返(RTT) **问题:** 如果分开执行,需要 3 次网络请求。 ```yaml 网络延迟时间线(RTT = Round Trip Time) 分开执行方式: 请求1: Java → Redis → Java (读) ← 耗时 T 请求2: Java → Redis → Java (判断) ← 耗时 T (耗时 3T) 请求3: Java → Redis → Java (写) ← 耗时 T 总耗时: 3T 用 Lua 脚本方式: 请求1: Java → Redis → Java (整个脚本) ← 耗时 T 总耗时: T 节省 66% 的网络开销! ``` #### 场景 3:在 Redis 中完成复杂的业务逻辑 **问题:** 有些操作逻辑复杂,如果来回折腾网络太低效。 ```text 传统方式: Java代码 ← 网络 → Redis ← 网络 → Java代码 ← 网络 → Redis ... (多轮交互,耗时且出错风险高) Lua方式: Java代码 → Redis(一次)→ [Lua脚本完整执行] → Java代码 (一轮交互,快速且原子) ``` ### 2.2 Lua 的核心优势总结 | 优势 | 说明 | 示例 | | --- | --- | --- | | **原子性** | 脚本从开始到结束不会被中断 | Redis 限流、抢购防超卖 | | **减少 RTT** | 一次网络请求完成多个操作 | 省去多次 Redis 往返 | | **内置Redis命令** | 能直接调用 redis.call() | 不需要在 Java 和 Redis 之间来回 | | **轻量级** | 文件小、启动快 | 脚本仅几KB,秒级加载 | | **可嵌入** | 可集成到其他程序 | Redis、Nginx、游戏引擎都内置Lua | --- ## 第三部分:Lua 的语言设计(学习基础) ### 3.1 Lua 的设计哲学 Lua 的设计者说过一句名言: > "Do one thing, and do it well." > > 不要什么都会,但要把该会的事做好。 ```text Lua设计原则: ┌─────────────────────────────────┐ │ 1. 简单易学 (语法少) │ │ └─ 没有class、没有接口等 │ │ │ │ 2. 快速执行 (高效) │ │ └─ 字节码+VM优化 │ │ │ │ 3. 轻量级 (资源少) │ │ └─ Lua解释器仅440KB │ │ │ │ 4. 可嵌入 (容易集成) │ │ └─ C/C++可轻松调用 │ │ │ │ 5. 灵活 (适应性强) │ │ └─ 动态类型、table等 │ └─────────────────────────────────┘ ``` ### 3.2 Lua 与其他语言的对比 ```text 语言特性对比表 Lua Python Java ──────────────────────────────── 简洁度 ★★★★★ ★★★★ ★★ 学习难度 ★★ ★★★ ★★★★ 执行速度 ★★★★ ★★★ ★★★★★ 内存占用 ★★★★★ ★★★ ★★ 集成难度 ★★ ★★★★ ★★★★ ──────────────────────────────── 结论: Lua最适合"嵌入场景" ``` ### 3.3 Lua 的核心数据结构 Lua 只有 **一种复杂数据结构**:Table(表) ```lua -- 几乎所有复杂数据都是 Table -- 1. 数组(Array) local arr = {1, 2, 3, 4, 5} -- 2. 字典(Dictionary / Hash Map) local dict = {name = "Tom", age = 30} -- 或者 local dict = {} dict["key1"] = "value1" -- 3. 对象(Object)- 本质还是 Table local person = { name = "Tom", age = 30, greet = function(self) print("Hello, I'm " .. self.name) end } -- 4. 其他都是Table的变体 ``` **关键认知:** Lua 用"简单"换"灵活"。没有专门的 class/interface,但 Table 足以表达一切。 --- ## 第四部分:Lua 在 Redis 中的应用(实战重点) ### 4.1 Redis 如何执行 Lua 脚本 #### 执行流程 ```text ┌──────────────────────────────────────────────────────────┐ │ Java 应用端 │ └────────────────────┬─────────────────────────────────────┘ │ 1. 加载脚本文件 │ (或直接传脚本文本) │ ┌────────────▼──────────────┐ │ 2. 调用 redis.script.load │ │ 或 redis.script.evalsha│ └────────────┬──────────────┘ │ │ 3. 网络传输 │ (脚本+参数) │ ┌────────────────────▼──────────────────────────────────────┐ │ Redis 服务端 │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ 4. Lua虚拟机加载脚本 │ │ │ │ (Redis 内置的 Lua 5.1) │ │ │ └────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ┌────────────────▼─────────────────────────────────────┐ │ │ │ 5. 执行脚本逻辑 │ │ │ │ • 解析KEYS参数 │ │ │ │ • 解析ARGV参数 │ │ │ │ • 调用redis.call()操作 │ │ │ │ • 执行业务逻辑(if/for等) │ │ │ │ • 返回结果 │ │ │ │ │ │ │ │ 【关键】所有操作在一个事务中 → 原子性保证 │ │ │ └────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ┌────────────────▼─────────────────────────────────────┐ │ │ │ 6. 返回结果给 Java │ │ │ │ (可以是 string/number/table) │ │ │ └────────────────┬─────────────────────────────────────┘ │ └────────────────────┬──────────────────────────────────────┘ │ │ 7. Java接收结果 ▼ ┌──────────────────────────┐ │ 继续业务逻辑处理 │ └──────────────────────────┘ ``` ### 4.2 关键概念:EVALSHA vs EVAL ```text 两种执行方式对比 方式1:EVAL (传脚本文本) Java → redis.eval("local x = ...", keys, args) │ └─ 优点:简单直观 缺点:每次都传脚本,网络浪费 方式2:EVALSHA (传脚本哈希) 步骤1:redis.script.load(脚本) ← 加载一次,返回SHA1 步骤2:redis.evalsha(SHA1, keys, args) ← 后续都用哈希 │ ├─ 优点:网络高效,脚本只传一次 └─ 缺点:需要先加载 在高并发场景,通常用 EVALSHA: • 初始化时 script.load() 一次 • 后续直接 evalsha(),节省网络 ``` ### 4.3 Lua 与 Redis 命令的交互 ```lua -- checkNeedCaptcha.lua 中的典型交互 -- 1. 读数据 local count = redis.call('get', counter_count_key) -- 2. 写数据 redis.call('set', counter_count_key, count) -- 3. 设置过期时间 redis.call('expire', verify_captcha_id, 60) -- 4. 条件判断(Lua语言,不是Redis命令) if count > 10 then redis.call('set', flag, 'yes') else redis.call('set', flag, 'no') end -- 5. 返回结果给Java return 'true' -- 或 'false' ``` **关键分工:** - **Lua语言部分**(if/for/等):脚本逻辑 - **redis.call()**:调用Redis命令 - **原子性**:整个脚本执行过程中,其他客户端的请求被暂停 --- ## 第五部分:Lua 脚本的原子性保证(核心价值) ### 5.1 为什么原子性很重要? #### 问题场景:没有原子性 ```java // Java 中分开执行(非原子) int count = redis.get("counter"); // 读:count=10 // 【时间点A】其他线程可能已修改 counter if (count > 10) { // 判断:10 > 10? false redis.set("flag", "yes"); // 不执行 } redis.set("flag", "no"); // 执行 // 问题:虽然我们在Java里判断是<=10, // 但实际上Redis中的counter可能已经是20了 // 逻辑产生了矛盾! ``` #### 解决方案:用 Lua 脚本(原子) ```lua -- 整个脚本从START到END,其他客户端看不到中间过程 -- 要么全部执行,要么全部不执行 -- START ───────────────────────────────────── local count = redis.call('get', 'counter') -- 读 if count > 10 then -- 判断 redis.call('set', 'flag', 'yes') -- 写 else redis.call('set', 'flag', 'no') -- 写 end return 'done' -- END ─────────────────────────────────────── -- 【保证】上面的所有操作,对其他客户端是透明的 -- 其他客户端要么看到执行前,要么看到执行后 -- 看不到中间过程 ``` ### 5.2 原子性的实现机制 ```yaml Redis的单线程架构 + Lua脚本 Redis服务端(单线程) 时间线: T1: 客户端1 发来 redis.set('a', 1) ├─ Redis执行 ├─ 完成 ✓ │ T2: 客户端2 发来 Lua脚本(10行) ├─ Redis 加载脚本到虚拟机 ├─ 执行第1行 (redis.call('get', 'x')) ├─ 执行第2行 (if判断) ├─ 执行第3行 (redis.call('set', 'y', ...)) ├─ ... ├─ 执行第10行 (return) ├─ 脚本执行完成 ✓ │ 【关键】中间没有其他客户端的命令插入! │ T3: 客户端3 发来 redis.get('y') ├─ Redis执行 ├─ 完成 ✓ ``` **结论:** Redis单线程 + Lua脚本组合 = 天然原子性 ### 5.3 对比:Java中如何实现类似的原子性 ```java // 方式1:使用同步块(不推荐,在应用层) private Object lock = new Object(); synchronized(lock) { int count = redis.get("counter"); if (count > 10) { redis.set("flag", "yes"); } else { redis.set("flag", "no"); } } // 问题: // 1. 只能保护Java的操作,Redis还是并发的 // 2. 锁持续时间长(包括网络延迟),竞争激烈 // 3. 多个Java实例无法共享锁 // 方式2:使用Redis Lua脚本(推荐!) redis.evalsha(scriptSha, keys, args); // 优点: // 1. 保护了Redis操作(真正的原子) // 2. 锁持续时间短(脚本执行时间毫秒级) // 3. 全局有效(多实例共享Redis) ``` --- ## 第六部分:大麦网案例深度解析 ### 6.1 checkNeedCaptcha.lua 为什么需要 Lua? ```text 场景:全局限流,决定是否需要验证码 需求分析: ┌─────────────────────────────────────────┐ │ 1. 读取全局计数器 │ │ 2. 获取上次重置时间 │ │ 3. 判断是否过了1秒 │ │ 4. 如果过了,重置计数器 │ │ 5. 对计数器+1 │ │ 6. 判断是否超过阈值(10) │ │ 7. 根据结果,写入标记('yes'或'no') │ │ 8. 返回结果给Java │ └─────────────────────────────────────────┘ 如果分开执行(Java+Redis多次往返): Java代码 Redis ├─ 发命令1 ────────────> │ GET COUNTER_COUNT │<────────────────────(网络延迟) │ ├─ 发命令2 ────────────> │ GET COUNTER_TIMESTAMP │<────────────────────(网络延迟) │ ├─ 【Java判断逻辑】 │ (计算count, 判断1秒) │ ├─ 发命令3 ────────────> │ SET COUNTER_COUNT │<────────────────────(网络延迟) │ ├─ 发命令4 ────────────> │ SET VERIFY_CAPTCHA_ID │<────────────────────(网络延迟) 总耗时:网络延迟 × 4 = 4T(非常慢!) 用 Lua 脚本执行: Java代码 Redis ├─ 发脚本 ────────────> │ (包含所有逻辑) │ │<────────────────────(一次往返) │ 脚本执行完,返回结果 总耗时:网络延迟 × 1 = T(快4倍!) 而且【原子性】保证: - 读数据、判断、写标记 一气呵成 - 其他请求看不到中间过程 - 防止了并发竞争导致的计数错误 ``` ### 6.2 Lua 脚本的结构分析 ```lua -- 第1部分:接收参数 local counter_count_key = KEYS[1] local verify_captcha_threshold = tonumber(ARGV[1]) -- 第2部分:强制开启检查(紧急防御) if always_verify_captcha == 1 then redis.call('set', verify_captcha_id, 'yes') return 'true' end -- 第3部分:获取当前状态(读操作) local count = tonumber(redis.call('get', counter_count_key) or "0") local lastResetTime = tonumber(redis.call('get', counter_timestamp_key) or "0") -- 第4部分:时间窗口处理(业务逻辑) if current_time_millis - lastResetTime > 1000 then count = 0 redis.call('set', counter_count_key, count) end -- 第5部分:阈值判定(核心逻辑) count = count + 1 if count > verify_captcha_threshold then redis.call('set', verify_captcha_id, 'yes') return 'true' else redis.call('set', verify_captcha_id, 'no') return 'false' end ``` **为什么这个逻辑不能在Java中做?** ```properties 关键原因:Redis 中的操作不能中断 如果逻辑分散在Java和Redis中: 时刻T1: Java获取count=10 时刻T2: 【其他客户端发来请求】 Redis的count变为11、12、13... Java还在做判断... 时刻T3: Java判断:count(10) > 10? No redis.set(flag, 'no') 问题:实际count已经是13了,但我们设置的是'no' 逻辑错误! 用Lua脚本,整个过程是原子的: 时刻T1: [Lua脚本开始执行] ├─ 读count=10 ├─ 【其他客户端请求来临,但被暂停】 ├─ count+1=11 ├─ 判断11 > 10? Yes ├─ 设置标记'yes' [Lua脚本结束] 时刻T2: 【其他客户端的请求现在才执行】 保证逻辑正确! ``` ## 第七部分:Lua 实战最佳实践 ### 7.1 何时该用 Lua?(决策树) ```text 需要在Redis中执行操作吗? │ 是 │ ┌─────────────┴─────────────┐ │ │ 【只用一条命令?】 【需要多条命令】 / \ │ 是 否 【命令间有依赖关系?】 │ / \ │ 是 否 │ │ │ 不用Lua 【需要原子性?】 可以分开执行 直接用Redis / \ 不用Lua 命令 是 否 │ │ │ 用Lua! 用Lua! 一般不用Lua (高效) (推荐) (e.g.日志) 实践判断标准: ┌────────────────────────────────────────┐ │ ✓ 用 Lua 的场景(高优先级) │ │ │ │ 1. 需要原子性 → 限流、库存、抢购 │ │ 2. 多条命令相互依赖 → 读-判-写 │ │ 3. 减少网络往返 → 高并发场景 │ │ 4. 业务逻辑复杂 → 多个判断分支 │ │ 5. 数据一致性关键 → 金融场景 │ │ │ │ 例子:verify_captcha, 防超卖, 库存减 │ └────────────────────────────────────────┘ ┌────────────────────────────────────────┐ │ ✗ 不用 Lua 的场景 │ │ │ │ 1. 简单操作 → SET/GET/INCR │ │ 2. 独立操作 → 日志、统计计数 │ │ 3. 逻辑全在应用层 → 数据处理 │ │ 4. 网络不敏感 → 低频操作 │ │ │ │ 例子:用户点赞数+1, 日志记录 │ └────────────────────────────────────────┘ ``` ### 7.2 开发流程:从编写到运行 ```java Step 1: 编写脚本文件 ──────────────────── 目录结构: src/main/resources/ └── lua/ └── checkNeedCaptcha.lua 脚本内容: local count = redis.call('get', KEYS[1]) or 0 count = count + 1 redis.call('set', KEYS[1], count) ... Step 2: Spring 初始化加载 ──────────────────────── @Component public class CheckNeedCaptchaOperate { private DefaultRedisScript redisScript; @PostConstruct public void init() { redisScript = new DefaultRedisScript<>(); // 从classpath加载脚本 redisScript.setScriptSource( new ResourceScriptSource( new ClassPathResource("lua/checkNeedCaptcha.lua") ) ); // 指定返回类型 redisScript.setResultType(String.class); } } 好处: • 项目启动时加载一次(1ms) • 后续使用EVALSHA,只需传哈希(高效) • 避免每次都读文件 Step 3: 执行脚本 ──────────────── public Boolean checkNeedCaptcha(List keys, String[] args) { // 调用脚本 Object result = redisCache.getInstance() .execute(redisScript, keys, args); // 结果处理 return Boolean.parseBoolean((String)result); } 参数说明: keys → Redis Key列表 [counter_count, counter_timestamp, verify_id] args → Lua参数列表 [10, 1700000000123, 60, 0] result → Lua脚本返回值 Step 4: 集成测试 ──────────────── @Test public void testCheckNeedCaptcha() { // 准备参数 List keys = Arrays.asList("counter", "timestamp", "flag_123"); String[] args = {"10", "1700000000000", "60", "0"}; // 执行脚本 Boolean result = captchaOperate.checkNeedCaptchaOperate(keys, args); // 验证结果 assertTrue(result); // 应该返回true或false // 验证Redis中的值 String flagValue = redis.get("flag_123"); assertEquals("yes", flagValue); } ``` ### 7.3 常见坑与避坑指南 ```sql ❌ 坑1:脚本太长太复杂(难以维护) 症状:脚本500行,if嵌套20层 原因:试图在脚本中完成所有业务逻辑 解决: • 保持脚本<100行 • 复杂逻辑放在Java中 • Lua只做"原子操作"部分 正确方式: Lua脚本: 只做 读-判-写 的原子化 Java代码: 负责复杂的业务逻辑、数据转换 ❌ 坑2:脚本返回复杂数据结构(序列化困难) 症状:return {user={name="Tom", age=30}} 问题:Lua的table转Java的Object很复杂 解决: • 只返回基本类型(string/number) • 复杂数据在Java层组装 好的实践: return 'true' -- 字符串 return '{"code":"ok"}' -- JSON字符串(Java parse) return 100 -- 数字 不好的实践: return {code=0, data={...}} -- 复杂table ❌ 坑3:忘记设置Key过期时间(内存泄漏) 症状:Redis内存不断增长 原因:created verify_captcha_id_123456 但没有EXPIRE 解决: redis.call('expire', key, ttl) -- 必须加! 完整模板: redis.call('set', myKey, myValue) redis.call('expire', myKey, 60) -- 60秒后自动删除 ❌ 坑4:脚本中有网络操作或阻塞操作(锁死Redis) 症状:Redis响应变慢,所有请求堆积 原因:脚本中调用HTTP接口、数据库等耗时操作 解决:禁止!脚本只能做本地运算和Redis操作 禁止清单: ✗ redis.call('HTTP_GET', url) -- 不存在 ✗ redis.call('SELECT FROM db...') -- 不存在 ✗ sleep(1000) -- 会锁死 ✗ io.open('file.txt') -- 文件I/O 允许清单: ✓ redis.call('GET', key) -- Redis操作 ✓ math.max(a, b) -- 本地运算 ✓ string.format() -- 字符串操作 ✓ table operations -- 表操作 ❌ 坑5:没有考虑脚本版本管理(升级困难) 症状:脚本更新后,老客户端仍用老版本 原因:没有版本控制机制 解决: • 使用EVALSHA方式,脚本哈希包含版本信息 • 发布脚本时做好日志记录 • 支持脚本灰度升级 版本管理方案: v1: checkNeedCaptcha_v1.lua (SHA1: abc123...) v2: checkNeedCaptcha_v2.lua (SHA1: def456...) 启动时: scriptSha_v2 = redis.script.load(v2_content) # 逐步升级,保证兼容性 ❌ 坑6:脚本执行超时(Redis卡顿) 症状:Redis管理员告诉你"你的脚本运行1分钟!" 原因:脚本中有死循环或过度复杂 解决: • 控制脚本逻辑复杂度 • 不要在脚本中做大规模数据处理 Redis脚本限制: 默认超时 = 5秒 超时后 → Redis强制杀死脚本 best practice: 脚本执行时间应该 < 100ms (毫秒级) ✓ 正确做法总结: 1. 脚本简洁 (<100行) 2. 只做原子操作 3. 返回基本类型 4. 记得设置过期时间 5. 禁止网络/IO操作 6. 做好版本管理 7. 监控脚本执行时间 ``` --- ## 第八部分:Lua vs 其他技术方案(对比分析) ### 8.1 原子性问题的5种解决方案 ```java 需求:在分布式系统中安全地执行"扣款100元" 方案1:Java synchronized(不推荐) ┌──────────────────────────────────────┐ │ synchronized(lock) { │ │ int balance = redis.get("account"); │ │ if (balance >= 100) { │ │ redis.set("account", balance-100);│ │ } │ │ } │ └──────────────────────────────────────┘ 分析: ✗ 只保护单机Java代码 ✗ Redis操作仍并发 ✗ 多实例互不影响,全局不安全 ✗ 锁持续时间长(包含网络延迟) ✗ 性能差,竞争激烈 适用场景:单机应用(现在几乎没人用) 方案2:数据库事务(传统方案) ┌──────────────────────────────────────┐ │ @Transactional │ │ public void debit() { │ │ Account acc = query("123"); │ │ acc.balance -= 100; │ │ save(acc); │ │ } │ └──────────────────────────────────────┘ 分析: ✓ 保证数据库级别原子性 ✓ 安全可靠 ✗ 每次都打数据库,网络往返多 ✗ 数据库连接池压力大 ✗ 高并发时容易超时 适用场景:小并发、强一致性要求高 不适合高并发实时场景 方案3:分布式锁 (Redlock) ┌──────────────────────────────────────┐ │ lock = redlock.lock("account_123"); │ │ try { │ │ balance = redis.get("account"); │ │ redis.set("account", balance-100); │ │ } finally { │ │ lock.unlock(); │ │ } │ └──────────────────────────────────────┘ 分析: ✓ 跨实例生效 ✓ Redis层面同步 ✗ Redlock实现复杂,易出bug ✗ 多线程竞争,吞吐量受限 ✗ 仍需多次网络往返 ✗ 存在死锁、超时等问题 适用场景:需要分布式锁的通用场景 但不是最优解 方案4:消息队列 (Queue) ┌──────────────────────────────────────┐ │ // 生产者 │ │ queue.send("debit_msg", { │ │ account_id: "123", │ │ amount: 100 │ │ }); │ │ │ │ // 消费者(单线程处理) │ │ msg = queue.consume(); │ │ balance = redis.get(account_id); │ │ redis.set(account_id, balance-100); │ └──────────────────────────────────────┘ 分析: ✓ 保证顺序性 ✓ 天然的原子操作(单线程消费) ✗ 有延迟(消息在队列中等待) ✗ 复杂(需要额外的MQ系统) ✗ 难以实时反馈结果给用户 适用场景:批量处理、异步操作 不适合实时扣款 方案5:Redis Lua脚本(最优!✓✓✓) ┌──────────────────────────────────────┐ │ redis.evalsha( │ │ "def123...", │ │ ["account_123"], │ │ ["100"] │ │ ); │ │ │ │ -- Lua脚本: │ │ local balance = redis.call('GET',..);│ │ if balance >= 100 then │ │ redis.call('SET',...,balance-100); │ │ return 'success' │ │ end │ └──────────────────────────────────────┘ 分析: ✓ Redis原生支持,无额外复杂度 ✓ 一次网络请求完成原子操作 ✓ 脚本简洁优雅 ✓ 性能最高 ✓ 天然的分布式安全 ✓ 实时返回结果 ✓✓✓ 高并发必选! 适用场景:高并发、实时、强一致性 限流、防超卖、库存系统 【结论】 方案1(synchronized) ← 不用 方案2(DB事务) ← 小并发 方案3(Redlock) ← 通用分布式 方案4(MQ) ← 异步场景 方案5(Lua) ← 高并发实时(最优选择!) 大麦网选择Lua的理由: • 高并发秒杀场景 • 需要真正的原子性 • 实时反馈验证结果 • 性能要求极高 → Lua完美匹配! ``` ### 8.2 执行效率对比 ```text 假设需要3个操作(READ → JUDGE → WRITE) 网络延迟 = 1ms/次 Redis执行时间 = 0.1ms 方案 操作流程 总耗时 网络往返 ────────────────────────────────────────────────────── synchronized read(1ms) judge(无网络) 3.1ms 3次 write(1ms) DB事务 query db(5ms) compute(无网络) 15.1ms 3次 update db(5ms) Redlock lock(1ms) read(1ms) 4.2ms 4次 write(1ms) unlock(1ms) MQ send msg(1ms) consume & process(50ms) 51ms 2次 [有延迟!] Lua脚本 evalsha(1ms) [脚本内:读-判-写] 1.3ms 1次 ← 最快! 性能对比图: 耗时 (ms) │ │ ■ Lua脚本 1.3ms ■ │ ■ Redlock 4.2ms ■■ │ ■ synch... 3.1ms ■■ │ ■ MQ 51ms ■■■■■■■■■■■■ │ ■ DB事务 15.1ms ■■■■ │ └──────────────────── Lua 的优势: • 网络往返最少(只需1次) • 执行时间最短(1.3ms) • 吞吐量最高(可达万级QPS) ``` --- ## 第九部分:Lua 的扩展应用(眼界开阔) ### 9.1 Lua 在其他领域的应用 ```nginx Lua不只在Redis中应用,它是一种通用脚本语言 ┌──────────────────────────────────────────────────┐ │ 1. 游戏引擎(最广泛应用) │ │ │ │ • Unity → C# (不用Lua) │ │ • Cocos2d-x → Lua (热门引擎) │ │ • Love2D → Lua原生 │ │ • 王者荣耀、梦幻西游都用Lua写业务逻辑 │ │ │ │ 优势:可以热更新(不用重编译) │ │ 这对游戏运营很重要 │ └──────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────┐ │ 2. Web服务器 (Nginx) │ │ │ │ nginx.conf: │ │ location /api/check { │ │ content_by_lua_block { │ │ if ngx.var.request_method == "POST" then │ local body = ngx.req.read_body() │ │ return ngx.say("ok") │ │ end │ │ } │ │ } │ │ │ │ 用途:网关层快速处理、限流、鉴权 │ │ 优势:避免请求到达Java,在网关层直接过滤 │ └──────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────┐ │ 3. 配置管理 (Ansible) │ │ │ │ playbook.yml: │ │ tasks: │ │ - name: Execute config script │ │ script: config.lua │ │ args: │ │ - "production" │ │ │ │ 用途:运维自动化脚本 │ └──────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────┐ │ 4. 数据库脚本 (Tarantool) │ │ │ │ Tarantool = Redis + Lua + DB │ │ 内存数据库,原生支持Lua,比Redis功能更强 │ │ 用于超高并发场景 │ └──────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────┐ │ 5. 智能家居 (NodeMCU) │ │ │ │ ESP8266芯片运行Lua │ │ 开发IoT设备的首选语言 │ └──────────────────────────────────────────────────┘ ``` ### 9.2 Redis 高级 Lua 应用示例 应用1:库存扣减(防超卖) ```lua -- inventory_deduct.lua -- 安全地扣减库存,防止超卖 local key = KEYS[1] -- 商品ID local amount = tonumber(ARGV[1]) -- 购买数量 local current = tonumber(redis.call('GET', key) or "0") if current >= amount then redis.call('DECRBY', key, amount) return 'success' else return 'insufficient' end ``` 应用2:限流通道(令牌桶) ```lua -- rate_limit.lua -- 令牌桶算法实现 local rate_key = KEYS[1] local rate_limit = tonumber(ARGV[1]) -- 100 tokens/sec local current_tokens = tonumber(redis.call('GET', rate_key) or rate_limit) if current_tokens > 0 then redis.call('DECR', rate_key) redis.call('EXPIRE', rate_key, 1) return 'allowed' else return 'blocked' end ``` 应用3:分布式会话锁 ```lua -- distributed_lock.lua -- 用Lua实现简单的分布式锁 local lock_key = KEYS[1] local lock_id = ARGV[1] local expire_time = tonumber(ARGV[2]) -- 原子地检查并设置 if redis.call('EXISTS', lock_key) == 0 then redis.call('SET', lock_key, lock_id) redis.call('EXPIRE', lock_key, expire_time) return 'locked' else return 'already_locked' end ``` 这些都是Lua在实战中的真实应用。 --- ## 第十部分:学习总结与进阶方向 ### 10.1 Lua 学习脑图 ```text ┌─── Lua是什么 │ ├─ 轻量级脚本语言 │ ├─ 可嵌入式设计 │ └─ 简洁高效 │ Lua掌握 ─┼─── 为什么用Lua │ ├─ 原子性保证 │ ├─ 减少网络往返 │ └─ 业务逻辑聚合 │ ├─── Lua基础语法 │ ├─ 数据类型(Table) │ ├─ 流程控制(if/for) │ └─ 函数定义 │ ├─── Redis中的应用 │ ├─ redis.call() │ ├─ EVAL vs EVALSHA │ └─ 脚本加载 │ ├─── 实战优化 │ ├─ 脚本简洁化 │ ├─ 参数配置化 │ └─ 性能监控 │ └─── 高级应用 ├─ 分布式锁 ├─ 限流算法 └─ 库存管理 ``` ### 10.2 进阶学习路线 ```text Level 1: 入门 (已掌握) □ Lua语法基础 □ Redis.call()使用 □ 简单的限流脚本 Level 2: 中级 (下一步) □ 脚本优化(减少网络往返) □ 脚本版本管理 □ 数据结构应用(集合、排序集) □ 错误处理 Level 3: 高级 (长期目标) □ 复杂算法实现(HyperLogLog、Bloom Filter) □ 多Lua脚本协作 □ Lua + Nginx 网关层应用 □ Tarantool(Redis++) Level 4: 架构 (深度应用) □ 高并发系统设计 □ 库存管理系统 □ 实时计费系统 □ 分布式事务 ``` ### 10.3 常用资源 ```text 推荐学习资源: 官方文档: • Redis Lua脚本官方文档 https://redis.io/commands/eval Lua语言教程: • Lua官网: https://www.lua.org/ • Lua 5.1参考手册 实战参考: • 大麦网源码(本项目) • Redis官方示例库 • Nginx + Lua教程 工具支持: • Redis CLI (redis-cli EVAL) • Lua IDE (ZeroBrane Studio) • 在线Lua编译器 进阶书籍: • 《Redis深度历险》 • 《Redis开发与运维》 • 《Nginx高性能Web服务器》 ``` --- ## 第十一部分:核心要点回顾(速记) ### 11.1 一张表总结 Lua ```text 维度 核心要点 ──────────────────────────────────────── 是什么 轻量级脚本,可嵌入到Redis中 为什么用 ✓ 原子性(不会被中断) ✓ 低延迟(一次网络往返) ✓ 高效率(减少RTT) 怎么用 redis.evalsha(脚本, KEYS, ARGV) 何时用 需要原子性 + 多个Redis操作 关键特性 ├─ redis.call()调用命令 ├─ 脚本从START到END无中断 ├─ 基于Redis单线程 └─ 返回简单类型给Java 常见模式 ├─ 限流(阈值判定) ├─ 防超卖(库存检查) ├─ 分布式锁(互斥) └─ 采样触发(柔性防护) 避坑指南 ├─ 脚本<100行(保持简洁) ├─ 禁止网络操作(会锁死) ├─ 记得设置过期(防泄漏) └─ 返回基本类型(易转换) 性能指标 ├─ 网络往返: 1次 ├─ 执行时间: <100ms ├─ 吞吐量: 万级QPS └─ 原子性: 100%保证 ``` ### 11.2 快速判断是否需要 Lua ```text 问题 判断 方案 ──────────────────────────────────── 单个Redis操作? 是 不用Lua 否 多个操作独立? 是 不用Lua 否 需要原子性? 是 ───→ 用Lua ✓ 否 业务逻辑简单? 是 可以不用 否 ───→ 用Lua ✓ 快速判断: "我需要在Redis中原子地执行多个操作吗?" 是 → 用Lua 否 → 不用 ``` ### 11.3 Lua 的"一图流"记忆法 ```text 【Java应用】 │ │ EVALSHA ▼ ┌─────────────────┐ │ Redis单线程 │ │ ┌───────────┐ │ │ │ Lua虚拟机 │ │ │ │ ┌─────────┴─┐│ │ │ │脚本执行 ││ │ │ │原子操作 ││ │ │ │无中断 ││ │ │ └─────────┬─┘│ │ └───────────┘ │ │ │返回结果 │ └────────┬────────┘ │ ┌─────▼─────┐ │返回值给Java│ └───────────┘ 记忆关键: - 单线程 + 脚本 = 原子性 - 原子性 + 高效 = 解决高并发 - 一次往返 + 无等待 = 极速体验 ``` --- ## 总结:为什么大麦网选择 Lua 回到原点,理解大麦网为什么在全局流量识别中使用 Lua: ```text 高并发秒杀场景: ├─ 1秒内可能有1万个注册请求 ├─ 需要快速判断:是否需要验证码 ├─ 决策依据:全局计数器值 └─ 结果:写入标记供下步使用 如果不用Lua: 1秒内1万个请求 ↓ (每个请求都读Redis) 1万次网络往返 ↓ (每次1ms延迟) 总耗时 10秒! ✗ 用了Lua: 1秒内1万个请求 ↓ (用EVALSHA执行脚本) 执行原子逻辑(在Redis内部) 计数准确,原子性保证 ↓ (EVALSHA只需1ms) 1秒完成 ✓ 这就是Lua被广泛应用在高并发系统中的根本原因! ``` --- ## 最后:一句话的精髓 > **Lua 是高并发系统的"快速通道":** > > 把多个操作合并成一个原子脚本,在 Redis 内部一次性执行,避免来回网络开销,保证数据一致性。 这就是 Lua 的全部精华! --- **企业级项目导航**:⬅️ [[05-JWT + Redis 双重会话管理 学习笔记|05-JWT + Redis 双重会话管理 学习笔记]] | 06-Lua 深度学习笔记 | ➡️ [[07-敏感数据保护:展示脱敏与加密存储深度解析|07-敏感数据保护:展示脱敏与加密存储深度解析]]